﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using Columbus.ActionResults;
using Columbus.Controls;

namespace Columbus
{
    /// <summary>
    /// Navigator is used to navigate to controller action, navigate to view or navigate back
    /// </summary>
    public class Navigator : INavigator
    {
        public const string DefaultNavigationFrameName = "Content";

        private readonly Func<string, Controller> controllerLocator;
        private readonly Func<string, FrameworkElement> viewLocator;

        private NavigationContext currentContext;

        private Frame frame;

        /// <summary>
        /// Gets or sets navigation frame used by this navigator.
        /// </summary>
        /// <value>Navigation frame.</value>
        public Frame NavigationFrame
        {
            get { return frame; }
            set
            {
                frame = value;
                if (frame != null)
                    frame.NavigationCanceled += CancelNavigation;
            }
        }

        /// <summary>
        /// Gets name of navigation frame used by this navigator.
        /// </summary>
        /// <value>Name of navigation frame.</value>
        public string NavigationFrameName { get; private set; }

        private Journal navigationJournal;

        /// <summary>
        /// Initializes a new instance of the <see cref="Navigator"/> class.
        /// </summary>
        /// <param name="controllerLocator">Controller locator.</param>
        /// <param name="viewLocator">View locator.</param>
        /// <param name="frameName">Name of navigation frame.</param>
        public Navigator(
            Func<string, Controller> controllerLocator,
            Func<string, FrameworkElement> viewLocator,
            string frameName = DefaultNavigationFrameName)
        {
            NavigationFrameName = frameName;
            this.controllerLocator = controllerLocator;
            this.viewLocator = viewLocator;
        }

        public void Navigate(NavigationRequest request, NavigationDirection direction = NavigationDirection.Forward)
        {
            if (request == null) throw new ArgumentNullException("request");

            Dispatcher.Dispatch(() => NavigationFrame.OnNavigationStarted(direction));

            var controller = controllerLocator(request.ControllerName);

            Background.Execute(() =>
            {
                currentContext = new NavigationContext(this, NavigationAppication.ApplicationSettings, viewLocator, request);
                controller.Initialize(currentContext);
                var result = InvokeControllerAction(NavigationFrame, controller, request);
                result.Execute();
                if (currentContext.NeedToSaveRequestInJournal)
                    navigationJournal.PushEntry(request);
                Dispatcher.Dispatch(() => NavigationFrame.OnNavigationCompleted());
            });
        }

        public void NavigateToView(string viewName, object viewModel = null)
        {
            if (viewName == null) throw new ArgumentNullException("viewName");

            Background.Execute(() =>
                                   {
                                       var pageResult = new PageResult(viewName, viewModel, new NavigationContext(this,  NavigationAppication.ApplicationSettings, viewLocator));
                                       pageResult.Execute();
                                   });
        }

        public bool NavigateBack()
        {
            var lastRequest = navigationJournal.PopEntry();
            if (lastRequest == null)
                return false;
            var prevRequest = navigationJournal.PopEntry();
            if (prevRequest == null)
                return false;
            
            Navigate(prevRequest, NavigationDirection.Backward);

            return true;
        }

        public void Initialize(Frame navigationFrame, ITransientStore transientStore)
        {
            NavigationFrame = navigationFrame;
            navigationJournal = new Journal(NavigationFrameName).Initialize(transientStore);
        }

        public void Activate(ITransientStore transientStore, IApplicationSettings settings)
        {
            navigationJournal = (transientStore.Load<Journal>(String.Format(Journal.JournalKeyPattern, NavigationFrameName))
                ?? new Journal(NavigationFrameName)).Initialize(transientStore);
            
            var state = transientStore.Load<ViewState>(String.Format(ViewState.KeyPattern, NavigationFrameName));
            if (state == null) return;

            if (state.HasViewModel)
            {
                var viewModel = RestoreViewModel(state, this, settings);
                NavigateToView(state.ViewName, viewModel);
            }
            else
            {
                NavigateToView(state.ViewName);
            }
        }

        public void Deactivate(ITransientStore transientStore)
        {
            var navigationFrame = NavigationFrame;
            if (navigationFrame == null)
                throw new NavigationFrameNotFoundException(NavigationFrameName);

            CancelNavigation();

            var view = navigationFrame.Content as FrameworkElement;

            if (view != null && !String.IsNullOrEmpty(view.Name))
            {
                ViewState viewState;
                var viewModel = view.DataContext as ViewModel;

                if (viewModel != null)
                {
                    viewState = new ViewState(view.Name, NavigationFrameName, viewModel.GetType().AssemblyQualifiedName);
                    viewModel.Deactivate(viewState);
                }
                else
                {
                    viewState = new ViewState(view.Name, NavigationFrameName);
                }

                var knownTypes = viewState.ConstructorParameterValues.Where(v => v != null).Select(v => v.GetType())
                    .Union(viewState.PropertyValues.Where(v => v != null).Select(v => v.GetType()))
                    .Distinct()
                    .ToArray();

                viewState.Persist(transientStore, knownTypes);
            }

            navigationJournal.Persist();
        }

        private void CancelNavigation()
        {
            if (currentContext != null)
                currentContext.CancelAction();
        }

        private static object RestoreViewModel(ViewState state, INavigator navigator, IApplicationSettings settings)
        {
            try
            {
                var viewModelType = Type.GetType(state.ViewModelTypeName);
                var viewModel = state.ConstructorParameterValues.Any()
                                    ? Activator.CreateInstance(viewModelType,
                                                               OrderConstructorParameters(state, viewModelType))
                                    : Activator.CreateInstance(viewModelType);

                var activatableViewModel = viewModel as ViewModel;
                if (activatableViewModel != null)
                {
                    activatableViewModel.Navigator = navigator;
                    activatableViewModel.Activate(state, settings);
                }

                return viewModel;
            }
            catch(Exception ex)
            {
                throw new ViewModelRestoringException(ex);
            }
        }

        private static object[] OrderConstructorParameters(ViewState state, Type viewModelType)
        {
            var constructorFound = false;
            var paramValueList = new List<object>();
            foreach (var constructor in viewModelType.GetConstructors())
            {
                paramValueList = new List<object>();
                var constructorMatches = true;
                foreach (var parameter in constructor.GetParameters())
                {
                    var parameterName = parameter.Name;
                    var matchingName = state.ConstructorParameterNames.SingleOrDefault(name => String.Equals(name, parameterName, StringComparison.InvariantCultureIgnoreCase));
                    if (matchingName != null)
                    {
                        paramValueList.Add(state.GetConstructorParameter(matchingName));
                    }
                    else
                    {
                        constructorMatches = false;
                        break;
                    }
                }
                if (constructorMatches)
                {
                    constructorFound = true;
                    break;
                }
            }
            if (!constructorFound)
                throw new ConstructorNotFoundException(viewModelType, state.ConstructorParameterNames);

            return paramValueList.ToArray();
        }

        private static ActionResult InvokeControllerAction(Frame navigationFrame, Controller controller, NavigationRequest navigationRequest)
        {
            var possibleActions = controller
                .GetType()
                .GetMethods()
                .Where(m => m.Name == navigationRequest.ActionName && Matches(m, navigationRequest))
                .ToArray();

            if (!possibleActions.Any() || possibleActions.Length > 1)
                throw new ActionNotFoundException(navigationRequest.ActionName);

            var action = possibleActions.Single();
            
            if (action.GetCustomAttributes(false).Any(a => a.GetType() == typeof(CancelableAttribute)))
                Dispatcher.Dispatch(() => navigationFrame.IsAllowedToCancel = true);

            return action.Invoke(controller, navigationRequest.Parameters.Select(p => p.Value).ToArray()) as ActionResult;
        }

        private static bool Matches(MethodBase method, NavigationRequest request)
        {
            var parameters = method.GetParameters();
            if (parameters.Count() != request.Parameters.Count())
                return false;

            var methodParameters = parameters.ToArray();
            var navigationParameters = request.Parameters.ToArray();

            for (var index = 0; index < methodParameters.Length; index++)
            {
                var methodParameter = methodParameters[index];
                var navigationParameter = navigationParameters[index];
                var parameterValue = navigationParameter.Value;
                if ((parameterValue == null && methodParameter.ParameterType.IsValueType)
                    || (parameterValue != null && methodParameter.ParameterType != parameterValue.GetType()))
                    return false;
            }

            return true;
        }
    }
}